package edu.xhu.homepage.service.impl;


import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import cn.hutool.core.lang.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.hash.Funnels;
import edu.xhu.api.behavior.BehaviorClient;
import edu.xhu.homepage.bloomFilter.BillBloomFilter;
import edu.xhu.homepage.mapper.BiliRecommendBloomMapper;
import edu.xhu.homepage.mapper.BiliVideoDataMapper;
import edu.xhu.homepage.mapper.BiliVideoMapper;
import edu.xhu.homepage.service.BiliVideoDataService;
import edu.xhu.homepage.service.BiliVideoService;
import edu.xhu.model.biliuser.pojos.BiliUser;
import edu.xhu.model.common.constants.AdvertisementVideoConstants;

import edu.xhu.model.common.constants.RecommendConstants;
import edu.xhu.model.common.dtos.ResponseResult;
import edu.xhu.model.common.enums.AppHttpCodeEnum;
import edu.xhu.model.video.pojos.BiliRecommendBloom;
import edu.xhu.model.video.pojos.BiliVideo;
import edu.xhu.model.video.pojos.BiliVideoData;
import edu.xhu.model.video.vos.AdvertisementVideoVo;
import edu.xhu.utils.thread.BiliUserThreadLocalUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class BiliVideoServiceImpl extends ServiceImpl<BiliVideoMapper, BiliVideo> implements BiliVideoService {

    private final BiliVideoMapper biliVideoMapper;

    private final BiliRecommendBloomMapper biliRecommendBloomMapper;

    private final StringRedisTemplate redisTemplate;

    private final BiliVideoDataService biliVideoDataService;

    private final BiliVideoDataMapper biliVideoDataMapper;

    private final BehaviorClient behaviorClient;

    private final ReentrantLock lock = new ReentrantLock();
    private final Object advertisementVideoQueryLock = new Object();
    private String bitMapKey;

    /**
     * 轮播图的key=carouse_key
     * 轮播图广告查询,和轮播图热门查询在前端组合，一周更新一次
     * 0.检查redis缓存有没有,有直接返回,没有下一步
     * 1.加锁
     * 2.检查缓存里有没有,有直接返回,没有下一步
     * 3.用一种规则查询去DB查一堆视频出来
     * 4.刷新到Redis
     */
    @Override
    //@ApiOperation("获取轮播广告")
    //@RequestMapping("/carrouselAdvertisementMap")
    public ResponseResult<?> carrouselAdvertisementMap() {
        //定义一个AdvertisementVideoVo类型的list
        List<AdvertisementVideoVo> advertisementVideoList = null;
        //定义一个String类型的变量，用于存放广告视频的json
        String advertisementVideoJson = null;


        //从redis中获取广告视频的json
        //先查询A缓存
        advertisementVideoJson = this.redisTemplate.opsForValue().get(AdvertisementVideoConstants.ADVERTISEMENT_REDIS_KEY_A);
        //如果redis中没有广告视频的json，则从redis中获取广告视频的json
        //再查询B缓存
        if (advertisementVideoJson == null) {
            advertisementVideoJson = this.redisTemplate.opsForValue().get(AdvertisementVideoConstants.ADVERTISEMENT_REDIS_KEY_B);
            //如果redis中没有广告视频的json，则从mysql中获取广告视频的json
            if (advertisementVideoJson == null) {
                //加锁
                synchronized (advertisementVideoQueryLock) {
                    //从redis中获取广告视频的json
                    advertisementVideoJson = this.redisTemplate.opsForValue().get(AdvertisementVideoConstants.ADVERTISEMENT_REDIS_KEY_A);
                    //再查A缓存
                    //如果redis中没有广告视频的json，则从mysql中获取广告视频的json
                    if (advertisementVideoJson == null)
                        advertisementVideoJson = this.redisTemplate.opsForValue().get(AdvertisementVideoConstants.ADVERTISEMENT_REDIS_KEY_B);
                    //如果redis中没有广告视频的json，则从mysql中获取广告视频的json
                    //再查B缓存
                    if (advertisementVideoJson == null) {
                        Map<String, Object> maps = getAdvertisementVideosJson();
                        //从mysql中获取广告视频的json
                        advertisementVideoJson
                                = maps.get("json").toString();
                        //将广告视频的json存储到redis中
                        this.redisTemplate.opsForValue().set(AdvertisementVideoConstants.ADVERTISEMENT_REDIS_KEY_A, advertisementVideoJson);
                        this.redisTemplate.opsForValue().set(AdvertisementVideoConstants.ADVERTISEMENT_REDIS_KEY_B, advertisementVideoJson);
                    }
                }
            }
        }
        //如果redis中没有广告视频的json，则返回错误结果
        if (advertisementVideoJson == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST, "广告位招标，or redis，mysql都失败了");
        }
        //将广告视频的json转换为AdvertisementVideoVo类型的list
        advertisementVideoList = JSONUtil.toBean(advertisementVideoJson, new TypeReference<List<AdvertisementVideoVo>>() {
        }, true);
        //返回广告视频的list
        return ResponseResult.okResult(advertisementVideoList);
    }

    /**
     * 计算广告视频数据并返回 JSON 字符串
     *
     * @return 广告视频的JSON字符串
     */
    private Map<String, Object> getAdvertisementVideosJson() {
        Map<String, Object> maps = new HashMap<>();
        //存储广告视频所有信息
        //<videoid,AdvertisementVideoVo>
        // 存储视频ID (videoid) 与 AdvertisementVideoVo 对象的对应关系
        // 键 (Long): 视频ID
        // 值 (AdvertisementVideoVo): 包含视频信息的对象
        Map<Long, AdvertisementVideoVo> advertisementVideoVoMap = new HashMap<>();

        //随机查5条视频模拟广告位
        List<BiliVideo> biliVideos = biliVideoMapper.random5VideoList();

        //拿到广告视频的所有 id
        List<Long> videoIds = biliVideos.stream()
                .map(BiliVideo::getVideoId)
                .collect(Collectors.toList());

        //拿到广告视频的数据信息，即BiliVideoData
        List<BiliVideoData> biliVideoDatas = biliVideoDataService.lambdaQuery().in(BiliVideoData::getVideoId, videoIds).list();

        //打包需要缓存的数据至advertisementVideoVoMap
        //封装BiliVideoData数据
        //使用 foreach 循环遍历并操作每个对象
        biliVideoDatas.forEach(biliVideoData -> {
            AdvertisementVideoVo advertisementVideoVo = new AdvertisementVideoVo();
            advertisementVideoVo.setBiliVideoData(biliVideoData);
            advertisementVideoVoMap.put(biliVideoData.getVideoId(), advertisementVideoVo);
        });

        //封装BiliVideo数据
        biliVideos.forEach(biliVideo -> {
            AdvertisementVideoVo advertisementVideoVo = advertisementVideoVoMap.get(biliVideo.getVideoId());
            if (advertisementVideoVo == null) {
                // TODO: 处理没有对应广告视频的情况
                System.out.println("No matching advertisement video found for BiliVideo " + biliVideo.getVideoId());
            } else {
                advertisementVideoVo.setBiliVideo(biliVideo);
            }
        });
        // 将 map 的value值转换为 List, 拿到广告视频数据list
        List<AdvertisementVideoVo> advertisementVideoVoList = new ArrayList<>(advertisementVideoVoMap.values());


        //将广告视频数据转换为json字符串
        String jsonStr = JSONUtil.toJsonStr(advertisementVideoVoList);
        maps.put("json", jsonStr);
        //拿一手videoId
        maps.put("videoIds", videoIds);

        return maps;
    }

    /**
     * 白名单函数,缓存预热,从MySQl查询最新的100条普通,50条合集
     * 1.缓存预热的问题,对于所有用户来说,看到的都是一样的,因为你走了缓存
     * 2.想让每个用户看到不一样的,怎么办,答案是:不加预热
     * 3.默认态,有缓存预热,直接走缓存.用户态,没有缓存预热,第一次打DB,后续打缓存
     */
    @PostConstruct
    @Transactional
    public void init() {
        //游客推荐缓存,默认,没有登录,大家推荐一样的
        if (BiliUserThreadLocalUtil.getUser() == null || BiliUserThreadLocalUtil.getUser().getUserId() == 999L) {
            List<BiliVideo> randomNormal = biliVideoMapper.normalVideoList();
            List<BiliVideo> randomCompilation = biliVideoMapper.compilationVideoList();
            if (randomNormal.isEmpty() || randomCompilation.isEmpty()) {
                throw new RuntimeException();
            }
            //缓存到redis
            redisTemplate.opsForList().rightPushAll(RecommendConstants.randomNormal, randomNormal.stream().map(JSONUtil::toJsonStr).collect(Collectors.toList()));
            redisTemplate.opsForList().rightPushAll(RecommendConstants.randomCompilation, randomCompilation.stream().map(JSONUtil::toJsonStr).collect(Collectors.toList()));
        }
    }

    /**
     * 基于布隆过滤器的随机视频推荐version_1
     * 0.每次推荐查询,查5x3共15个视频
     * 1.每次推荐都是15条不同视频
     * 2.推荐5条合集视频,推荐10条普通视频
     * 3.推荐视频不缓存,只看布隆里有没有
     * 4.为进一步保证推荐视频不同,重试超5次,返回没有更多视频了
     */
    @Override
    @Transactional
    public ResponseResult<?> videoRecommendation(){
        //0.缓存值
        List<BiliVideo> resultVideoForNormal = new ArrayList<>();
        List<BiliVideo> resultVideoForCompilation = new ArrayList<>();
        //1.初始参数,用来区分游客态,用户态
        String norMalKey = RecommendConstants.randomNormal;
        String compilationKey = RecommendConstants.randomCompilation;
        if (BiliUserThreadLocalUtil.getUser() != null && !(BiliUserThreadLocalUtil.getUser().getUserId() == 999L)) {
            //如果有用户,登录状态,找用户自己的key
            norMalKey = RecommendConstants.randomNormalUser + BiliUserThreadLocalUtil.getUser().getUserId();
            compilationKey = RecommendConstants.randomCompilationUser + BiliUserThreadLocalUtil.getUser().getUserId();
        }
        //2.根据userId->new一个布隆实例
        BiliUser user = BiliUserThreadLocalUtil.getUser();
        if (user == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST, "user错误！");
        }
        this.bitMapKey = "bloom:" + user.getUserId();
        BillBloomFilter<Long> FilterNormal = new BillBloomFilter<>(
                this.bitMapKey, 5, Funnels.longFunnel(), redisTemplate);
        BillBloomFilter<Long> FilterCompilation = new BillBloomFilter<>(
                this.bitMapKey, 5, Funnels.longFunnel(), redisTemplate);
        try {
            initBloom(user);
        } catch (RuntimeException e) {
            return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR, "初始化bloom失败！");
        }
        //3.双剑枷锁,查视频先走缓存
        ResponseResult<?> normalRes = doubleCheckedLocking(resultVideoForNormal, norMalKey);
        ResponseResult<?> compilationRes = doubleCheckedLocking(resultVideoForCompilation, compilationKey);
        if (!normalRes.getCode().equals(AppHttpCodeEnum.SUCCESS.getCode())) {
            if (!compilationRes.getCode().equals(AppHttpCodeEnum.SUCCESS.getCode())) {
                return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
            }
        }
        //3.没报错,resultVideoForNormal和resultVideoForCompilation就有数据了,有数据的执行推荐
        CompletableFuture<List<BiliVideo>> future = null;
        if (!resultVideoForNormal.isEmpty()) {
            //4.开一个异步线程,执行执行普通推荐
            int normalCount = 5;
            String key = norMalKey;
            future = CompletableFuture.supplyAsync(() -> recommendation(resultVideoForNormal, FilterNormal, normalCount, key));
        }
        List<BiliVideo> recommendationCompilation = null;
        if (!resultVideoForCompilation.isEmpty()) {
            //5.主线程继续执行
            int compilationCount = 5;
            recommendationCompilation = recommendation(resultVideoForCompilation, FilterCompilation, compilationCount, compilationKey);
        }
        //6.等待结果
        List<BiliVideo> resAll = new ArrayList<>();
        if (future != null) {
            resAll.addAll(future.join());
        }
        if (recommendationCompilation != null) {
            resAll.addAll(recommendationCompilation);
        }
        List<Map<String, Object>> resultList = new ArrayList<>();
        if (!resAll.isEmpty()) {
            resAll.forEach(biliVideo -> {
                Map<String, Object> resultMap = new HashMap<>();
                resultMap.put("biliVideo", biliVideo);
                BiliVideoData biliVideoData = biliVideoDataMapper.selectById(biliVideo.getVideoId());
                if (biliVideoData != null) {
                    resultMap.put("biliVideoData", biliVideoData);
                }
                resultList.add(resultMap);
            });
        }
        //7.TODO记录一下推荐视频
        //8.返回数据不存在
        return ResponseResult.okResult(resultList);
    }

    private void initBloom(BiliUser user) {
        //判断一下有没有当前态的布隆,没有则从DB找并set一个并设置过期时间,-2:不存在的key,-1:存在key,没有ttl,0:还没过期
        try {
            Long expire = redisTemplate.getExpire(bitMapKey);
            if (expire != null) {
                //不存在这个key(说明是新用户,或者一天没刷视频了),找DB设置一个key并设置过期时间
                if (expire.equals((long) -2)) {
                    //先从DB找
                    BiliRecommendBloom bloom = biliRecommendBloomMapper.selectOne(Wrappers.<BiliRecommendBloom>lambdaQuery().eq(BiliRecommendBloom::getUserId, 999L));
                    if (bloom == null) {
                        redisTemplate.opsForValue().setBit(bitMapKey, 0L, false);
                    } else {
                        //把bloom的byte[]设置给bitMapKey
                        if (bloom.getBitmap() == null) {
                            //DB也没找到
                            redisTemplate.opsForValue().setBit(bitMapKey, 0L, false);
                        } else {
                            //问题1:DB找到了,刷到redis(存在一个问题,DB是旧的,因为上次推荐的因为某种原因还没有刷到DB),怎么办?
                            //答案是:找到了就保证是最新的,因为你调用这一次推荐,上次推荐必定是mysql插入成功才返回数据！没有并发需求
                            //问题2:我设置DB刷新时间,3-5分钟内才刷新,那这最后的时间,用户没有刷视频,怎么办?怎么保证持久化?
                            //答案是:不管怎样,都设置key6小时过期,如果key失效,表明用户6小时没刷视频,调度更新mysql是保证最新的.
                            //问题3:这样做,如果Redis挂了,这期间,怎么办?Redis挂了,key没有,从DB找,DB里没刷新,还是旧的?
                            redisTemplate.execute((RedisCallback<Void>) connection -> {
                                byte[] bytesMap = Base64.getDecoder().decode(bloom.getBitmap());
                                connection.set(bitMapKey.getBytes(), bytesMap);
                                return null;
                            });
                        }
                    }
                }
                //不管怎么样,都刷新一下过期时间,存6小时,保证缓存失效,mysql里是最新bitmap
                //注:游客的bloom不设过期时间
                if (user.getUserId() != 999L) {
                    redisTemplate.expire(bitMapKey, Duration.ofHours((long) (Math.random() * 6 + 1)));
                }
            } else {
                throw new RuntimeException();
            }
        } catch (RuntimeException e) {
            log.error("bitMapKey初始化失败:{}", e.toString());
        }

    }

    /**
     *
     */
    private ResponseResult<?> doubleCheckedLocking(List<BiliVideo> videos, String key) {
        //推荐请求打进来,如果是默认态,当然有缓存,random=true
        //如果是用户态,当然没有缓存,random=false
        Boolean random = redisTemplate.hasKey(key);
        if (random == null || !random) {
            lock.lock();
            random = redisTemplate.hasKey(key);
            if (random == null || !random) {
                //走DB
                List<BiliVideo> dbList = null;
                if (key.contains("normal")) {
                    dbList = biliVideoMapper.normalVideoList();
                }
                if (key.contains("compilation")) {
                    dbList = biliVideoMapper.compilationVideoList();
                }
                if (dbList != null) {
                    //刷新缓存
                    redisTemplate.opsForList().rightPushAll(key, dbList.stream().map(JSONUtil::toJsonStr).collect(Collectors.toList()));
                    random = redisTemplate.hasKey(key);
                }
            } else {
                //缓存有了
                Long size = redisTemplate.opsForList().size(key);
                if (size != null) {
                    List<String> range = redisTemplate.opsForList().range(key, 0, size - 1L);
                    if (range != null) {
                        List<BiliVideo> collect = range.stream().map(str -> JSONUtil.toBean(str, new TypeReference<BiliVideo>() {
                        }, true)).collect(Collectors.toList());
                        videos.addAll(collect);
                        return ResponseResult.okResult(videos);
                    } else {
                        return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
                    }
                } else {
                    return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
                }
            }
            lock.unlock();
        }
        if (random == null || !random) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
        }
        //缓存有了
        Long size = redisTemplate.opsForList().size(key);
        if (size == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
        }
        List<String> range = redisTemplate.opsForList().range(key, 0, size - 1L);
        if (range == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
        }
        List<BiliVideo> collect = range.stream().map(str -> JSONUtil.toBean(str, new TypeReference<BiliVideo>() {
        }, true)).collect(Collectors.toList());
        videos.addAll(collect);
        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }

    /**
     * 封装统一方法,进行推荐
     */
    private List<BiliVideo> recommendation(List<BiliVideo> allVideos, BillBloomFilter<Long> billBloomFilter, int count, String type) {

        int tmpCount = count;
        List<BiliVideo> tmpVideo = new ArrayList<>();   //记录成功视频增量
        while (tmpCount > 0 && !allVideos.isEmpty()) {
            int preSize = tmpVideo.size();
            //后续,修改SQL,加这段代码,现在加死循环
//            if (allVideos.isEmpty()) {
//                //视频不足！再刷一次
//                if (count > 5) {
//                    allVideos = biliVideoMapper.normalVideoList();
//                } else {
//                    allVideos = biliVideoMapper.compilationVideoList();
//                }
//                //还是不足,直接返回tmp中所有成功的
//                if (allVideos == null || allVideos.isEmpty()) {
//                    return tmpVideo;
//                }
//            }
            //最外部,用管道！返回每个对象是否命中hash
            //返回前tmpCount个结果
            List<BiliVideo> subAllVideos;
            if (allVideos.size() < tmpCount) {
                //不够这么多
                subAllVideos = allVideos;
            } else {
                subAllVideos = allVideos.subList(0, tmpCount);
            }
            subAllVideos.forEach(subVideo -> billBloomFilter.addKeysToQueryCache(subVideo.getVideoId()));
            //做布隆计算,返回每个video是否成功
            Map<Long, Boolean> booleanMapSuccess = billBloomFilter.checkKeysInBloomFilterWithPipeline();
            AtomicInteger i = new AtomicInteger(0);
            booleanMapSuccess.forEach((k, v) -> {
                if (!v) {
                    if (subAllVideos.get(i.get()).getVideoId().equals(k)) {
                        tmpVideo.add(subAllVideos.get(i.get()));
                        billBloomFilter.addKeysToInsertCache(k);
                    }
                }
                i.getAndIncrement();
            });
            billBloomFilter.insertKeysToBloomFilterWithPipeline();
            //ok了,最后的步骤
            redisTemplate.executePipelined((RedisCallback<?>) connection -> {
                //把sub中的video都pop掉
                for (int j = 0; j < subAllVideos.size(); j++) {
                    connection.lPop(type.getBytes());
                }
                return null;
            });
            allVideos.removeAll(subAllVideos);
            tmpCount -= (tmpVideo.size() + preSize);   //减去成功个数(减掉上次成功的)
        }
        return tmpVideo;
    }

    /**
     * 获取主页视频详情
     * 1.判断videoId
     * 2.根据videoId查DB
     * 3.查到？封装返回详情信息
     */
    @Override
    public ResponseResult<?> videoDetail(Long videoId) {
        if (videoId == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_REQUIRE);
        }
        BiliVideo biliVideo = getOne(Wrappers.<BiliVideo>lambdaQuery().eq(BiliVideo::getVideoId, videoId));
        if (biliVideo == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
        }
        //存在视频.1:返回视频数据,2:返回弹幕数据,3:返回用户数据,4:返回评论数据,5:返回视频数据

        return null;
    }

    /**
     * api:获取所有视频
     */
    @Override
    public ResponseResult<?> videosListApi() {
        return ResponseResult.okResult(JSONUtil.toJsonStr(list()));
    }


}
